/*
 * Licensed to the Tai Ping Jin Ke
 *
 * Copyright (c) 2022 .
 * All rights reserved.
 * 项目名称：太平金科-基础组件封装层-SpringBoot组件封装-核心依赖
 * 版权说明：本软件属太平金融科技服务(上海)有限公司所有，在未获得太平金融科技服务(上海)有限公司正式授权情况下，任何企业和个人，不能获取、阅读、安装、传播本软件涉及的任何受知识产权保护的内容。
 */
package com.taipingframework.boot.web.support;

import cn.hutool.core.io.IoUtil;
import cn.hutool.http.HTMLFilter;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.Option;
import com.jayway.jsonpath.spi.json.JacksonJsonNodeJsonProvider;
import com.taipingframework.json.JsonParser;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.LinkedList;
import java.util.Map;
import java.util.Objects;

/**
 * 对默认的 ServletInputStream对象 重新包装！
 */
public class RepeatableInputStream extends ServletInputStream {

    private final ByteArrayInputStream is;

    /**
     * 此处的处理步骤稍嫌繁琐，但却不得不为，如存在性能问题，则需思考优化方案！
     */
    RepeatableInputStream(ServletInputStream is, String characterEncoding) {
        // 默认行为，不对请求报文中的危险字符进行转义！
        // this.is = new ByteArrayInputStream(IoUtil.readBytes(is));

        // 1、获取请求报文
        //String requestBody = IoUtil.read(is, Charset.forName(getCharacterEncoding()));

        // 2、过滤掉字符串中的空格、回车、换行符、制表符【期望找到一个合适的正则表达式来处理json字符串以达到与下方编码相同的效果】
        //String payload = StringHelper.replaceBlank(requestBody);

        // 将第1步和第2步合并处理(主要目的是为了过滤掉字符串中的空格、回车、换行符、制表符，但可別把日期字符串中的空格给移除掉啊)
        LinkedList<String> lines = new LinkedList<>();
        IoUtil.readLines(is, Charset.forName(characterEncoding), lines);
        String payload = lines.stream().map(String::trim).reduce(String::concat).orElse("{}");

        // 3、构造JsonPath解析配置
        Configuration config = Configuration.builder()
                .jsonProvider(new JacksonJsonNodeJsonProvider())
                .options(Option.ALWAYS_RETURN_LIST, Option.SUPPRESS_EXCEPTIONS)
                .build();

        // 4、将JSON格式的请求报文数据缓存到JsonPath文档对象中
        DocumentContext payloadDocumentContext = JsonPath.using(config).parse(payload);

        // 5、通过递归算法构造JSON报文中每个属性名的路径并返回
        Map<String, Object> jsonPathValueMap = new JsonParser(payload).getPathValueMap();

        // 6、通过JsonPath原生api动态更新整个json文档 - 即，使用Hutool工具类HtmlUtil对客户端所有参数进行转义
        for (String key : jsonPathValueMap.keySet()) {
            payloadDocumentContext.map(key, (currentValue, configuration) -> {
                if (Objects.nonNull(currentValue) && currentValue instanceof CharSequence) {
                    return HTMLFilter.htmlSpecialChars((String) currentValue);
                }
                return currentValue;
            });
        }

        // 7、获取`值`转义后的JSON报文
        String requestBody = payloadDocumentContext.jsonString();

        // 8、重新构造可重复读的输入字节流对象
        this.is = new ByteArrayInputStream(StringUtils.getBytes(requestBody, Charset.forName(characterEncoding)));
    }

    @Override
    public int read() throws IOException {
        return this.is.read();
    }

    @Override
    public int read(@NotNull byte[] b) throws IOException {
        return this.is.read(b);
    }

    @Override
    public int read(@NotNull final byte[] b, final int off, final int len) throws IOException {
        return this.is.read(b, off, len);
    }

    @Override
    public int readLine(final byte[] b, final int off, final int len) throws IOException {
        return super.readLine(b, off, len);
    }

    @Override
    public boolean isFinished() {
        return this.is.available() == 0;
    }

    @Override
    public boolean isReady() {
        return true;
    }

    @Override
    public void setReadListener(ReadListener readListener) {
    }

    /**
     * ...
     */
    @Override
    public synchronized void mark(int readLimit) {
        this.is.mark(readLimit);
    }

    /**
     * ...
     */
    @Override
    public synchronized void reset() throws IOException {
        this.is.reset();
    }

    /**
     * ...
     */
    @Override
    public boolean markSupported() {
        return this.is.markSupported();
    }

}
